﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;
using uam_fps_game.Utils;
using Microsoft.Xna.Framework.Content;
using uam_fps_game.Cameras;
using System.Collections;
using uam_fps_game.Gfx;
using Microsoft.Xna.Framework.Graphics.PackedVector;

namespace uam_fps_game.Gfx
{
    public class Renderer
    {
        public Matrix ViewMatrix;
        public Matrix ProjectionMatrix;
        public Vector3 EyePosition;

        protected struct ModelData : IComparable
        {
            public ModelMeshPart modelMeshPart;
            public Material material;
            public Light[] affectingLights;
            public Matrix matrix;
            public float distanceFromCamera;

            public int CompareTo(object obj)
            {
                float f1 = this.distanceFromCamera;
                float f2 = ((ModelData)obj).distanceFromCamera;
                if (f1 < f2) return 1;
                else if (f1 > f2) return -1;
                else return 0;
            }
        }

        protected struct BillboardData : IComparable
        {
            public Billboard billboard;
            public float distanceFromCamera;

            public int CompareTo(object obj)
            {
                float f1 = this.distanceFromCamera;
                float f2 = ((BillboardData)obj).distanceFromCamera;
                if (f1 < f2) return 1;
                else if (f1 > f2) return -1;
                else return 0;
            }
        }

        protected struct ParticleSystemData : IComparable
        {
            public ParticleSystem particleSystem;
            public float distanceFromCamera;

            public int CompareTo(object obj)
            {
                float f1 = this.distanceFromCamera;
                float f2 = ((ParticleSystemData)obj).distanceFromCamera;
                if (f1 < f2) return 1;
                else if (f1 > f2) return -1;
                else return 0;
            }
        }

        protected List<ModelData> mOpaqueModels;
        protected List<ModelData> mTransparentModels;

        protected List<BillboardData> mOpaqueBillboards;
        protected List<BillboardData> mTransparentBillboards;

        static internal GraphicsDevice mGraphicsDevice;
        protected List<ParticleSystemData> mOpaqueParticleSystems;
        protected List<ParticleSystemData> mTransparentParticleSystems;

        protected Effect mSimpleEffect;
        protected Effect mShadowMapEffect;
        protected SimpleEffectManager mSEManager;

        protected DepthStencilState mDepthReadWrite;
        protected DepthStencilState mDepthRead;

        static internal int mShadowMapSize = 2048;

        public Renderer(GraphicsDevice device, IServiceProvider serviceProvider)
        {
            mGraphicsDevice = device;
            ContentManager contentLoader = new ContentManager(serviceProvider, "Content");

            mOpaqueModels = new List<ModelData>();
            mTransparentModels = new List<ModelData>();

            mOpaqueBillboards = new List<BillboardData>();
            mTransparentBillboards = new List<BillboardData>();

            mOpaqueParticleSystems = new List<ParticleSystemData>();
            mTransparentParticleSystems = new List<ParticleSystemData>();
            
            mSimpleEffect = contentLoader.Load<Effect>("Shaders\\SimpleEffect");
            //mShadowMapEffect = contentLoader.Load<Effect>("Shaders\\ShadowMapRenderer");

            mSEManager = new SimpleEffectManager(mSimpleEffect);

            mDepthReadWrite = DepthStencilState.Default;

            mDepthRead = new DepthStencilState();
            mDepthRead.DepthBufferWriteEnable = false;

            Billboard.SetGraphicsDevice(mGraphicsDevice);
            Billboard.LoadEffect(contentLoader);
        }


        /// <summary>
        /// Processes and renders the supplied object data.
        /// </summary>
        /// <param name="visibleAreaData">The VisibleAreaData instance containing object data to be drawn.</param>
        internal void Render(VisibleAreaData visibleAreaData)
        {
            ProcessData(visibleAreaData);
            //RenderShadowMaps(visibleAreaData);
            RenderModels();

            Billboard.SetMatrices(ViewMatrix, ProjectionMatrix);
            RenderBillboards();
            RenderParticleSystems();
        }

        private void ProcessData(VisibleAreaData visibleAreaData)
        {
            // models (and lights)
            for (int i = 0; i < visibleAreaData.Models.Count; i++)
            {
                if (!visibleAreaData.Models[i].Visible)
                    continue;

                foreach (ModelMesh mesh in visibleAreaData.Models[i].Model.Model.Meshes)
                {
                    for (int j = 0; j < visibleAreaData.Models[i].Model.MeshPartCount; j++)
                    {
                        ModelData data = new ModelData();

                        data.modelMeshPart = mesh.MeshParts[j];
                        data.material = visibleAreaData.Models[i].Model.GetMeshPartMaterial(j);
                        Matrix world = visibleAreaData.Models[i].WorldMatrix;  // moze to poprawic jakos?
                        //Matrix.Multiply(ref visibleObjectsData.Objects[i].Model.BoneTransforms[mesh.ParentBone.Index], ref visibleObjectsData.Objects[i].WorldMatrix, out data.matrix);
                        Matrix.Multiply(ref visibleAreaData.Models[i].Model.BoneTransforms[mesh.ParentBone.Index], ref world, out data.matrix);

                        data.affectingLights = new Light[visibleAreaData.Models[i].LightsNum];
                        for (int k = 0; k < visibleAreaData.Models[i].LightsNum; k++)
                        {
                            data.affectingLights[k] = visibleAreaData.Lights[visibleAreaData.Models[i].LightIndices[k]];
                        }

                        if (data.material.IsTransparent)
                        {
                            data.distanceFromCamera = Vector3.Distance(EyePosition, data.matrix.Translation);
                            mTransparentModels.Add(data);
                        }
                        else
                        {
                            data.distanceFromCamera = -1;
                            mOpaqueModels.Add(data);
                        }
                    }
                }

            }

            mTransparentModels.Sort();

            // billboards
            for (int i = 0; i < visibleAreaData.Billboards.Count; i++)
            {
                if (!visibleAreaData.Billboards[i].Visible)
                    continue;

                BillboardData data = new BillboardData();
                data.billboard = visibleAreaData.Billboards[i];
                if (data.billboard.IsTransparent)
                {
                    data.distanceFromCamera = Vector3.Distance(EyePosition, data.billboard.WorldMatrix.Translation);
                    mTransparentBillboards.Add(data);
                }
                else
                {
                    data.distanceFromCamera = -1;
                    mOpaqueBillboards.Add(data);
                }
            }

            mTransparentBillboards.Sort();

            // particlesystems
            for (int i = 0; i < visibleAreaData.ParticleSystems.Count; i++)
            {
                if (!visibleAreaData.ParticleSystems[i].Visible)
                    continue;

                ParticleSystemData data = new ParticleSystemData();
                data.particleSystem = visibleAreaData.ParticleSystems[i];
                //if (data.particleSystem.IsTransparent)
                //{
                    data.distanceFromCamera = Vector3.Distance(EyePosition, data.particleSystem.WorldMatrix.Translation);
                    mTransparentParticleSystems.Add(data);
                /*}
                else
                {
                    data.distanceFromCamera = -1;
                    mOpaqueParticleSystems.Add(data);
                }*/
            }

            mTransparentParticleSystems.Sort();
        
        }

        private void RenderShadowMaps(VisibleAreaData visibleAreaData)
        {
            foreach (Light light in visibleAreaData.Lights)
            {
                if (light.IsGeneratingShadows)
                {
                    light.CalcMatrices();
                    light.ShadowMap = CreateShadowMap(mTransparentModels, mOpaqueModels, light);
                }
            }
        }

        private void RenderModels()
        {
            mSEManager.SetViewData(ViewMatrix, ProjectionMatrix, EyePosition, Light.AmbientColorPower);

            

            //Preparing the device for opaque object rendering.
            mGraphicsDevice.DepthStencilState = mDepthReadWrite;
            mGraphicsDevice.BlendState = BlendState.Opaque;

            foreach (ModelData data in mOpaqueModels)
            {
                Matrix world = data.matrix;  // Czy to jest bez sensu (nieoptymalne) czy mi sie tylko wydaje?
                mSEManager.SetObjectData(ref world, data.material);
                mSEManager.SetLightData(data.affectingLights);
                //mSEManager.SetShadowMap(shadowMap);
                foreach (EffectPass pass in mSimpleEffect.CurrentTechnique.Passes)
                {
                    pass.Apply();
                    mGraphicsDevice.SetVertexBuffer(data.modelMeshPart.VertexBuffer);
                    mGraphicsDevice.Indices = data.modelMeshPart.IndexBuffer;
                    mGraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, data.modelMeshPart.VertexOffset, 0, data.modelMeshPart.NumVertices, data.modelMeshPart.StartIndex, data.modelMeshPart.PrimitiveCount);
                }

            }
            mOpaqueModels.Clear();

            //Preparing the device for transparent object rendering.
            mGraphicsDevice.DepthStencilState = mDepthRead;
            mGraphicsDevice.BlendState = BlendState.AlphaBlend;

            foreach (ModelData data in mTransparentModels)
            {
                Matrix world = data.matrix;  // Czy to jest bez sensu (nieoptymalne) czy mi sie tylko wydaje?
                mSEManager.SetObjectData(ref world, data.material);
                mSEManager.SetLightData(data.affectingLights);

                foreach (EffectPass pass in mSimpleEffect.CurrentTechnique.Passes)
                {
                    pass.Apply();
                    mGraphicsDevice.SetVertexBuffer(data.modelMeshPart.VertexBuffer);
                    mGraphicsDevice.Indices = data.modelMeshPart.IndexBuffer;
                    mGraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, data.modelMeshPart.VertexOffset, 0, data.modelMeshPart.NumVertices, data.modelMeshPart.StartIndex, data.modelMeshPart.PrimitiveCount);
                }

            }
            mTransparentModels.Clear();
        }

        private void RenderBillboards()
        {
            foreach (BillboardData billboardData in mOpaqueBillboards)
            {
                billboardData.billboard.Render();
            }
            mOpaqueBillboards.Clear();
            foreach (BillboardData billboardData in mTransparentBillboards)
            {
                billboardData.billboard.Render();
            }
            mTransparentBillboards.Clear();
        }

        private Texture2D CreateShadowMap(List<ModelData> dataList, List<ModelData> dataList2, Light light)
        {
            RenderTarget2D shadowRenderTarger = new RenderTarget2D(mGraphicsDevice, mShadowMapSize, mShadowMapSize, false, SurfaceFormat.Single, DepthFormat.Depth24);

            DepthStencilState depthStencilState = new DepthStencilState();
            depthStencilState.DepthBufferFunction = CompareFunction.LessEqual;

            mGraphicsDevice.DepthStencilState = depthStencilState;

            mGraphicsDevice.SetRenderTarget(shadowRenderTarger);
            mGraphicsDevice.Clear(Color.Black);

            mShadowMapEffect.Parameters["View"].SetValue(light.LightView);
            mShadowMapEffect.Parameters["Projection"].SetValue(light.LightProjection);
            foreach (ModelData data in dataList)
            {
                mShadowMapEffect.Parameters["World"].SetValue(data.matrix);
                foreach (EffectPass pass in mShadowMapEffect.CurrentTechnique.Passes)
                {
                    pass.Apply();
                    mGraphicsDevice.SetVertexBuffer(data.modelMeshPart.VertexBuffer);
                    mGraphicsDevice.Indices = data.modelMeshPart.IndexBuffer;
                    mGraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, data.modelMeshPart.VertexOffset, 0, data.modelMeshPart.NumVertices, data.modelMeshPart.StartIndex, data.modelMeshPart.PrimitiveCount);
                }

            }

            foreach (ModelData data in dataList2)
            {
                mShadowMapEffect.Parameters["World"].SetValue(data.matrix);
                foreach (EffectPass pass in mShadowMapEffect.CurrentTechnique.Passes)
                {
                    pass.Apply();
                    mGraphicsDevice.SetVertexBuffer(data.modelMeshPart.VertexBuffer);
                    mGraphicsDevice.Indices = data.modelMeshPart.IndexBuffer;
                    mGraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, data.modelMeshPart.VertexOffset, 0, data.modelMeshPart.NumVertices, data.modelMeshPart.StartIndex, data.modelMeshPart.PrimitiveCount);
                }

            }

            mGraphicsDevice.SetRenderTarget(null);
            mGraphicsDevice.Clear(Color.BurlyWood);


            return shadowRenderTarger;
        }

        private void RenderParticleSystems()
        {
            foreach (ParticleSystemData pass in mTransparentParticleSystems)
            {
                pass.particleSystem.SetCamera(ViewMatrix, ProjectionMatrix);
                pass.particleSystem.Draw();
            }
            mTransparentBillboards.Clear();
        }
    }
}
